/*
File.cpp
*/

#include "FileSystem.h"
#include "File.h"
#include "Settings.h"

/*
When a file is created, it is given an inode, but its size is 0 and it
has no data blocks.
Important:
Before using a newly created file, check that the inode is not NULL.
*/
File::File(FileSystem * fs_loc, unsigned int inode_id_loc):
    fs(fs_loc)
{
    set_inode(inode_id_loc);
    Seek(0);
}

/*
Returns:
    the number of bytes read.
*/
unsigned int File::Read(unsigned int size, char * buf){

    unsigned int bytes_to_read;
    DataBlock * data_block;
    unsigned int r;

    if(inode->size == 0 || size == 0) return 0;

    //limit the number of bytes they can read based on the file position ptr and the file's size
    if(cur_file_byte+size-1 > inode->size-1){
        size = inode->size-cur_file_byte;        
    }

    bytes_to_read = size;
    while(bytes_to_read > 0){
    
        //load the block into the cache (or access it)
        data_block = fs->cache->GetDataBlock(cur_block_id);

        //try to read all the bytes from the current block (Read may return saying we couldn't read that many from this block)
        r = data_block->Read(cur_block_byte,bytes_to_read,buf+(size-bytes_to_read));
        bytes_to_read-=r;
        //increment the file position pointer r bytes forward (updates cur_block_id and cur_block_offset)
        Seek(cur_file_byte+r);
    }

    return (size-bytes_to_read);
}

/*
Returns:
    the number of bytes written.
*/
unsigned int File::Write(unsigned int size, char * buf){

    unsigned int bytes_to_write;
    DataBlock * data_block;
    unsigned int r;
    int ri;

    bytes_to_write = size;

    while(bytes_to_write > 0){

        if(cur_block_byte == Settings::BlockSize){
            //get a new data block
            data_block = fs->get_free_data_block();
            if(data_block == NULL){
                return (size-bytes_to_write);
            }
            
            //update inode info
            ri = new_data_block(data_block->id);
            if(ri != 0){
                return (size-bytes_to_write);
            }

            //set file position ptr so we are writing to the beginning of the new block
            cur_file_byte = inode->size;
            cur_block_id = data_block->id;
            cur_block_byte = 0;
        }else{
            //still room to write in the current data block
            //load the block into the cache (or access it)
            data_block = fs->cache->GetDataBlock(cur_block_id);
        }
    
        r = data_block->Write(cur_block_byte,bytes_to_write,(buf+size-bytes_to_write));

        bytes_to_write-=r;

        //update the file size if we added new data
        if(r+cur_file_byte > inode->size){
            inode->size += (r+cur_file_byte - inode->size);
        }
        //increment the file position pointer r bytes forward (updates cur_block_id and cur_block_offset)
        Seek(cur_file_byte+r);
    }

    //inode was modified, write it to the file
    inode->last_mod_ts = time(NULL);
    fs->write_inode(this);

    return (size-bytes_to_write);
}

/*
Description:
    Sets the private variables that store the position info.
Arguments:
    file_offset - bytes offset from beginning of this file's data
Returns:
    0 on success
    -1 on error (seeking past end of file)
*/
int File::Seek(unsigned int file_offset){

    unsigned int block_number;

    if(file_offset > inode->size){
        printf("Error File::Seek - attempted to seek past the end of the file\n");
        return -1;
    }

    //a.) byte offset in this file
    cur_file_byte = file_offset;

    //b.) byte offset in the block
    //c.) calculate which data block this byte is in...
    cur_block_byte = file_offset%Settings::BlockSize;
    if(cur_block_byte == 0 && file_offset == inode->size){
        //next write gets a new block
        cur_block_byte = Settings::BlockSize;
        cur_block_id = 0;
    }else{
        block_number = file_offset/Settings::BlockSize;
        cur_block_id = data_block_ids[block_number];
    }

    return 0;
}
        
/*
called when the last reference is unlinked
Allows the data blocks and inode to be reused
*/
int File::FinalUnlink(){

    unsigned int i;
    int r;

    //allow the data-blocks to be re-used
    for(i=0;i<data_block_ids.size();i++){
        fs->superblock[data_block_ids[i]] = '\0';
    }
    r = fs->write_superblock();
    if(r != 0){
        return -1;
    }

    //allow the inode to be used for a future file
    r = fs->set_inode_to_free(inode_id);
    if(r != 0){
        return -1;
    }

    return 0;

}

/*****************
Start private methods
******************/

/*
Does not error check that the new_inode_id is valid.
sets the inode_id, inode pointer, and fills data_block_ids
*/
void File::set_inode(unsigned int new_inode_id){

    unsigned int i,c1,c2;
    DataBlock * data_block1,* data_block2;
    //d = direct, id1 = singly-indirect, id2 = doubly-indirect
    unsigned int num_d_ptrs, num_id1_ptrs, num_id2_ptrs;

    unsigned int num_blocks;
    unsigned int bids_per_block; //number of unsigned int (block ids) that can fit in a data block
    unsigned int * block_id;

    //easy stuff, set the id, inode, and clear the old list of data blocks
    inode_id = new_inode_id;
    inode = fs->inodes[inode_id];
    data_block_ids.clear();

    //see how many of the inodes->ptrs are currently used
    get_num_ptrs(&num_d_ptrs,&num_id1_ptrs,&num_id2_ptrs);

    num_blocks = inode->num_blocks;
    bids_per_block = Settings::BlockSize / sizeof(unsigned int);

    //cout << "file with inode " << inode_id << " has " << num_d_ptrs << " d ptrs" << endl;

    //read the direct block ids
    for(i=0;i<num_d_ptrs;i++){
        data_block_ids.push_back(inode->ptrs_dir[i]);
        num_blocks--;
    }

    //read the singly-indirect block ids
    for(i=0;i<num_id1_ptrs;i++){
        data_block1 = fs->cache->GetDataBlock(inode->ptrs_indir1[i]);
        //read block ids in this data block
        c1 = 0;
        while(c1<bids_per_block && num_blocks > 0){
            block_id = (unsigned int *)(data_block1->Data+c1*sizeof(unsigned int));
            data_block_ids.push_back(*block_id);
            c1++;
            num_blocks--;
        }
    }

    //read the doubly-indirect block ids
    for(i=0;i<num_id2_ptrs;i++){
        //get the doubly-indirect data block
        data_block2 = fs->cache->GetDataBlock(inode->ptrs_indir2[i]);

        //read block ids in this data block
        c2 = 0;
        while(c2<bids_per_block && num_blocks > 0){
            block_id = (unsigned int *)(data_block2->Data+c2*sizeof(unsigned int));

            //the block with id block_id contains the actual data block addresses that we want
            data_block1 = fs->cache->GetDataBlock(*block_id);
            c1 = 0;
            while(c1<bids_per_block && num_blocks > 0){
                block_id = (unsigned int *)(data_block1->Data+c1*sizeof(unsigned int));
                data_block_ids.push_back(*block_id);
                c1++;
                num_blocks--;
            }
            c2++;
        }
    }

    return;
}

/*
Desc:
    Called when a write causes the file to be stored in a new data block.
    This is given its own function because it is kinda long and complicated.
    The new data block may be accessed through a direct pointer in the inode.
    Or, it may be access through a singly or doubly indirect pointer.
    In this case, an additional data block may be created.
    The calling function should call FileSystem::write_inode (to avoid unnecessary writes).
    note: in the code, 'bid' stands for block id
Args:
    data_block_id - the id of the new data block
Returns:
    0 on success
    -1 on error
*/
int File::new_data_block(unsigned int data_block_id){

    unsigned int bids_per_block;
    unsigned int num_blocks;
    unsigned int num_d_ptrs, num_id1_ptrs, num_id2_ptrs;
    unsigned int num_bids, num_bids_outer;
    DataBlock * data_block, * data_block_outer;
    unsigned int tmp_id;
    unsigned int num_ba;
    unsigned int r;

    num_blocks = inode->num_blocks;

    //see how many of the inodes->ptrs are currently used
    get_num_ptrs(&num_d_ptrs,&num_id1_ptrs,&num_id2_ptrs);

    bids_per_block = Settings::BlockSize/sizeof(unsigned int);

    if(num_d_ptrs < Settings::InodeNumDirPtrs){
        //easy, we can have a direct pointer to this block
        inode->ptrs_dir[num_blocks] = data_block_id;
        inode->num_blocks++;
        data_block_ids.push_back(data_block_id);
        return 0;
    }

    //try to use a singly indirect ptr if we don't have any doubly indirect ptrs yet
    if(num_id2_ptrs == 0){

        //calculate how many block ids are in the block pointed to by the last singly indirect pointer
        if(num_id1_ptrs == 0){
            //trigger getting a new singly indirect ptr block
            num_bids = bids_per_block;
        }else{
            num_bids = num_blocks - (Settings::InodeNumDirPtrs + bids_per_block*(num_id1_ptrs-1));
        }

        data_block = NULL;
        if(num_bids != bids_per_block){
            //put this block in the block pointed to by the last singly indirect pointer
            data_block = fs->cache->GetDataBlock(inode->ptrs_indir1[num_id1_ptrs-1]);
        }else if(num_id1_ptrs < Settings::InodeNumIndir1Ptrs){
            //create a new singly indirect ptr block
            data_block = fs->get_free_data_block();
            inode->ptrs_indir1[num_id1_ptrs] = data_block->id;
            num_bids = 0;
        }

        if(data_block != NULL){
            //put the new block's id in the block pointed to by the last id1 ptr
            data_block->Write(num_bids*sizeof(unsigned int),sizeof(unsigned int),(char*)&data_block_id);
            inode->num_blocks++;
            data_block_ids.push_back(data_block_id);
            return 0;
        }
    }

    //try to use a doubly indirect ptr
    if(num_id2_ptrs == 0){
        /*
        create the first doubly indirect ptr (consists of an 'outer' block and an 'inner' block
        inodes->ptrs_indir2[#] = outer-block-id
        the outer block contains addresses of inner blocks
        the inner blocks contain addresses of the actual data blocks
        */
        data_block_outer = fs->get_free_data_block();
        inode->ptrs_indir2[0] = data_block_outer->id;

        data_block = fs->get_free_data_block();
        data_block_outer->Write(0,sizeof(unsigned int),(char *)&data_block->id);
        data_block->Write(0,sizeof(unsigned int),(char *)&data_block_id);

    }else{
        //the number of 'blocks accessible' through the outer block pointed to by the last id2 ptr
        num_ba = num_blocks - (Settings::InodeNumDirPtrs + Settings::InodeNumIndir1Ptrs*bids_per_block + bids_per_block*bids_per_block*(num_id2_ptrs-1));
        
        //the number of inner block id's stored in the outer block pointed to by the last id2 ptr
        num_bids_outer = (num_ba+bids_per_block-1)/bids_per_block; //ceil(num_ba/bids_per_block)
        
        //the number of data block id's stored in the last inner block
        num_bids = num_ba-(num_bids_outer-1)*bids_per_block;

        if(num_bids != bids_per_block){
            /*
            write to the inner block
            outer block id = inode->ptrs_indir2[num_id2_ptrs-1]
            */
            data_block_outer = fs->cache->GetDataBlock(inode->ptrs_indir2[num_id2_ptrs-1]);

            //get the inner block id by reading the last address from the outer block
            r = data_block_outer->Read((num_bids_outer-1)*sizeof(unsigned int), sizeof(unsigned int), (char *)&tmp_id);
            if(r != sizeof(unsigned int)){
                printf("Error File::new_data_block - could not read a block id from the doubly indirect pointer\n");
                return -1;
            }
            //get the inner block
            data_block = fs->cache->GetDataBlock(tmp_id);

            //write this new block id to it
            data_block->Write(num_bids*sizeof(unsigned int),sizeof(unsigned int),(char *)&data_block_id);

        }else{
            //inner block is full
            if(num_bids_outer != bids_per_block){
                data_block_outer = fs->cache->GetDataBlock(inode->ptrs_indir2[num_id2_ptrs-1]);

                //create a new inner block for this outer block
                data_block = fs->get_free_data_block();
                data_block_outer->Write(num_bids_outer*sizeof(unsigned int),sizeof(unsigned int),(char *)&data_block->id);
                data_block->Write(0,sizeof(unsigned int),(char *)&data_block_id);

            }else{
                //outer block has maximum number of inner blocks
                if(num_id2_ptrs < Settings::InodeNumIndir2Ptrs){
                    //create a new outer block
                    data_block_outer = fs->get_free_data_block();
                    inode->ptrs_indir2[num_id2_ptrs] = data_block_outer->id;

                    data_block = fs->get_free_data_block();
                    data_block_outer->Write(0,sizeof(unsigned int),(char *)&data_block->id);
                    data_block->Write(0,sizeof(unsigned int),(char *)&data_block_id);

                }else{
                    printf("Error File::new_data_block - not enought doubly-indirect pointers\n");
                    return -1;
                }
            }
        }
    }

    inode->num_blocks++;
    data_block_ids.push_back(data_block_id);
    return 0;
}

/*
Desc:
    sets the arguments to the number of direct, singly-indirect, and doubly-indirect pointers used to access all of the blocks (based on inodes->num_blocks)
*/
void File::get_num_ptrs(unsigned int * num_d_ptrs, unsigned int * num_id1_ptrs, unsigned int * num_id2_ptrs){

    //number of data blocks we get through the inode's ptrs
    unsigned int data_blocks_via_d_ptrs, data_blocks_via_id1_ptrs, data_blocks_via_id2_ptrs;

    //number of blocks accessible through the various types of pointers in the inode, direct ptrs always access 1 data block, but the others rely on blocksize
    unsigned int blocks_accessed_by_id1_ptr, blocks_accessed_by_id2_ptr; 

    unsigned int total_blocks;

    //init values that depend on Settings
    blocks_accessed_by_id1_ptr = Settings::BlockSize/sizeof(unsigned int);
    blocks_accessed_by_id2_ptr = Settings::BlockSize*Settings::BlockSize/sizeof(unsigned int)/sizeof(unsigned int);

    //calculate the number of direct, singly-indirect, and doubly-indirect blocks based off of inode->blocks
    total_blocks = inode->num_blocks;
    if(total_blocks > Settings::InodeNumDirPtrs){
        data_blocks_via_d_ptrs = Settings::InodeNumDirPtrs;
    }else{
        data_blocks_via_d_ptrs = total_blocks;
    }
    total_blocks-=data_blocks_via_d_ptrs;
    *num_d_ptrs = data_blocks_via_d_ptrs;

    if(total_blocks > Settings::InodeNumIndir1Ptrs*blocks_accessed_by_id1_ptr){
        data_blocks_via_id1_ptrs = Settings::InodeNumIndir1Ptrs*blocks_accessed_by_id1_ptr;
    }else{
        data_blocks_via_id1_ptrs = total_blocks;
    }
    total_blocks-=data_blocks_via_id1_ptrs;
    //ceil
    *num_id1_ptrs = (data_blocks_via_id1_ptrs+blocks_accessed_by_id1_ptr-1)/blocks_accessed_by_id1_ptr;

    if(total_blocks > Settings::InodeNumIndir2Ptrs*blocks_accessed_by_id2_ptr){
        data_blocks_via_id2_ptrs = Settings::InodeNumIndir2Ptrs*blocks_accessed_by_id2_ptr;
    }else{
        data_blocks_via_id2_ptrs = total_blocks;
    }
    total_blocks-=data_blocks_via_id2_ptrs;
    //ceil
    *num_id2_ptrs = (data_blocks_via_id2_ptrs+blocks_accessed_by_id2_ptr-1)/blocks_accessed_by_id2_ptr;

    if(total_blocks != 0){
        printf("Error File::set_inode - file has too many blocks, could not read them all\n");
    }

    return;
    
}
